package rescuecore2.worldmodel;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import rescuecore2.registry.Registry;

/**
 * Abstract base class for WorldModel implementations.
 * 
 * @param <T>
 *            The subclass of Entity that this world model holds.
 */
public abstract class AbstractWorldModel<T extends Entity> implements
		WorldModel<T> {

	private final Set<WorldModelListener<? super T>> listeners;
	private final Collection<Class<? extends T>> allowedClasses;

	/**
	 * Construct a new abstract world model.
	 */
	protected AbstractWorldModel() {
		listeners = new HashSet<WorldModelListener<? super T>>();
		allowedClasses = new HashSet<Class<? extends T>>();
	}

	@Override
	public final void addWorldModelListener(WorldModelListener<? super T> l) {
		synchronized (listeners) {
			listeners.add(l);
		}
	}

	@Override
	public final void removeWorldModelListener(WorldModelListener<? super T> l) {
		synchronized (listeners) {
			listeners.remove(l);
		}
	}

	@Override
	@SuppressWarnings("unchecked")
	public final void addEntity(Entity e) {
		if (isAllowed(e)) {
			addEntityImpl((T) e);
		} else {
			throw new IllegalArgumentException(this
					+ " does not accept entities of type "
					+ e.getClass().getName());
		}
	}

	@Override
	public final void addEntities(Collection<? extends Entity> e) {
		for (Entity next : e) {
			addEntity(next);
		}
	}

	@Override
	public final void removeEntity(T e) {
		removeEntity(e.getID());
	}

	/**
	 * Subclasses should provide their implementation of addEntity here.
	 * 
	 * @param t
	 *            The entity to add.
	 */
	protected abstract void addEntityImpl(T t);

	@Override
	public void merge(Collection<? extends Entity> toMerge) {
		for (Entity next : toMerge) {
			T existing = getEntity(next.getID());
			if (existing == null) {
				addEntity(next);
			} else {
				Set<Property> props = existing.getProperties();
				for (Property prop : props) {
					Property other = next.getProperty(prop.getURN());
					if (other.isDefined()) {
						prop.takeValue(other);
					}
				}
			}
		}
	}

	@Override
	public void merge(ChangeSet changeSet) {
		for (EntityID e : changeSet.getChangedEntities()) {
			Entity existingEntity = getEntity(e);
			boolean add = false;
			if (existingEntity == null) {
				// Construct a new entity
				existingEntity = Registry.getCurrentRegistry().createEntity(
						changeSet.getEntityURN(e), e);
				if (existingEntity == null) {
					// Bail out
					continue;
				}
				add = true;
			}
			for (Property p : changeSet.getChangedProperties(e)) {
				Property existingProperty = existingEntity.getProperty(p
						.getURN());
				existingProperty.takeValue(p);
			}
			if (add) {
				addEntity(existingEntity);
			}
		}
		for (EntityID next : changeSet.getDeletedEntities()) {
			removeEntity(next);
		}
	}

	/**
	 * Notify listeners that an entity has been added.
	 * 
	 * @param e
	 *            The new entity.
	 */
	protected final void fireEntityAdded(T e) {
		for (WorldModelListener<? super T> l : getListeners()) {
			l.entityAdded(this, e);
		}
	}

	/**
	 * Notify listeners that an entity has been removed.
	 * 
	 * @param e
	 *            The entity that has been removed.
	 */
	protected final void fireEntityRemoved(T e) {
		for (WorldModelListener<? super T> l : getListeners()) {
			l.entityRemoved(this, e);
		}
	}

	/**
	 * Find out if a particular Entity is allowed into this world model. The
	 * default implementation checks that the object is assignable to at least
	 * one class previously registered with {@link #registerAllowedClass(Class)}
	 * .
	 * 
	 * @param e
	 *            The entity to check.
	 * @return True if the entity is allowed, false otherwise.
	 */
	protected boolean isAllowed(Entity e) {
		for (Class<? extends T> next : allowedClasses) {
			if (next.isAssignableFrom(e.getClass())) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Register an allowed class.
	 * 
	 * @param clazz
	 *            The subclass of Entity that is allowed in this world model.
	 */
	protected void registerAllowedClass(Class<? extends T> clazz) {
		allowedClasses.add(clazz);
	}

	private Collection<WorldModelListener<? super T>> getListeners() {
		synchronized (listeners) {
			return new HashSet<WorldModelListener<? super T>>(listeners);
		}
	}
}
